package dane.brown.sql.checker;

import lombok.extern.log4j.Log4j2;
import org.apache.ibatis.builder.BuilderException;
import org.apache.ibatis.builder.xml.XMLMapperBuilder;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.ognl.OgnlException;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.type.TypeAliasRegistry;

import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
 * 修正版MyBatis语法检查工具（修复MappedStatement获取方式）
 */
@Log4j2
public class MyBatisOracleSyntaxChecker {

    // 检查规则：Oracle特有语法正则 + 错误提示 + MySQL替代方案
    private static final List<CheckRule> CHECK_RULES = new ArrayList<>();
    // 匹配MyBatis-Plus的ew表达式（如${ew.customSqlSegment}）
    private static final Pattern EW_EXPRESSION_PATTERN = Pattern.compile("\\$\\{ew\\.\\w+\\}");
    // 匹配所有${}表达式
    private static final Pattern DOLLAR_EXPRESSION_PATTERN = Pattern.compile("\\$\\{[^}]+\\}");
    // 匹配所有#{}表达式
    private static final Pattern HASH_EXPRESSION_PATTERN = Pattern.compile("#\\{[^}]+\\}");
    // 匹配XML标签
    private static final Pattern XML_TAG_PATTERN = Pattern.compile("<[^>]+>", Pattern.DOTALL);
    //必须匹配mapper文件，因为有可能会遇到其他格式的xml。
    private static final Pattern MYBATIS_MAPPER_PATTERN = Pattern.compile("<mapper\\s+", Pattern.CASE_INSENSITIVE);
    // 用于过滤编译目录的正则（如target/、build/等）
    private static final Pattern BUILD_DIR_PATTERN = Pattern.compile("(target|build|out)/", Pattern.CASE_INSENSITIVE);

    static {
        // 初始化检查规则（保持不变）
        CHECK_RULES.add(new CheckRule(
                "NVL\\(\\s*[^,]+\\s*,\\s*[^)]+\\s*\\)",
                "使用Oracle特有函数NVL",
                "替换为MySQL的IFNULL(表达式, 替代值)或COALESCE(表达式1, 表达式2...)"
        ));
        CHECK_RULES.add(new CheckRule(
                "TO_CHAR\\(\\s*(SYSDATE|SYSTIMESTAMP|\\w+)\\s*,\\s*['\"].+?['\"]\\s*\\)",
                "使用Oracle特有函数TO_CHAR",
                "日期格式化用DATE_FORMAT(日期, '%Y-%m-%d')；数值转字符串用CAST(数值 AS CHAR)"
        ));
        CHECK_RULES.add(new CheckRule(
                "WM_CONCAT\\(\\s*[^)]+\\s*\\)",
                "使用Oracle特有函数WM_CONCAT",
                "替换为GROUP_CONCAT(字段 SEPARATOR ',')，注意调整group_concat_max_len参数"
        ));
        CHECK_RULES.add(new CheckRule(
                "ROWNUM\\s+<=?\\s+\\d+",
                "使用Oracle分页语法ROWNUM",
                "替换为MySQL的LIMIT语法：LIMIT 数量 OFFSET 偏移量"
        ));
        CHECK_RULES.add(new CheckRule(
                "SYSDATE",
                "使用Oracle日期函数SYSDATE",
                "替换为MySQL的NOW()（日期时间）或CURDATE()（日期）"
        ));
        CHECK_RULES.add(new CheckRule(
                "\\|\\|",
                "使用Oracle字符串连接符||",
                "替换为CONCAT(a, b)或启用sql_mode=PIPES_AS_CONCAT（不推荐）"
        ));
        CHECK_RULES.add(new CheckRule(
                "DECODE\\(\\s*[^,]+\\s*,[^)]+\\)",
                "使用Oracle特有函数DECODE",
                "替换为CASE表达式：CASE WHEN 条件 THEN 值1 ELSE 值2 END"
        ));
        CHECK_RULES.add(new CheckRule(
                "CONNECT\\s+BY",
                "使用Oracle层级查询CONNECT BY",
                "MySQL 8.0+可用WITH RECURSIVE递归查询替代"
        ));
        CHECK_RULES.add(new CheckRule(
                "TRUNC\\(\\s*SYSDATE\\s*,\\s*['\"].+?['\"]\\s*\\)",
                "使用Oracle日期截断函数TRUNC",
                "例如TRUNC(SYSDATE,'DD')替换为DATE_FORMAT(NOW(), '%Y-%m-%d')"
        ));
        CHECK_RULES.add(new CheckRule(
                "(\\w+)\\.NEXTVAL|(\\w+)\\.CURRVAL",
                "使用Oracle序列（SEQUENCE）",
                "替换为MySQL自增主键(AUTO_INCREMENT)或自定义序列表"
        ));
    }

    public static void main(String[] args) {

        // 解析命令行参数（支持指定配置文件路径）
        String configFilePath = "oracle_rules.properties"; // 默认配置文件
        String dirPath = null;


        if (args.length == 0) {
            log.error("请指定MyBatis XML映射文件所在目录！");
            log.error("用法：java MyBatisOracleSyntaxChecker [目录路径]");
            return;
        }

        for (int i = 0; i < args.length; i++) {
            if ("-c".equals(args[i]) && i + 1 < args.length) {
                configFilePath = args[i + 1];
                i++; // 跳过配置文件值
            } else {
                dirPath = args[i];
            }
        }
        if (dirPath == null) {
            System.out.println("请指定MyBatis XML映射文件所在目录！");
            System.out.println("用法：java MyBatisOracleSyntaxChecker [-c 配置文件路径] [目录路径]");
            System.out.println("示例：java MyBatisOracleSyntaxChecker -c custom_rules.properties ./src/main/resources");
            return;
        }
        File dir = new File(dirPath);
        if (!dir.exists() || !dir.isDirectory()) {
            System.err.println("错误：目录不存在或不是有效目录 -> " + dirPath);
            return;
        }

        // 加载检查规则配置文件
        if (!loadCheckRules(configFilePath)) {
            System.err.println("错误：无法加载检查规则配置文件，程序退出");
            return;
        }

        // 初始化MyBatis配置（跳过类验证）
        Configuration configuration = new Configuration() {
            @Override
            public TypeAliasRegistry getTypeAliasRegistry() {
                return new TypeAliasRegistry() {
                    @Override
                    public <T> Class<T> resolveAlias(String alias) {
                        try {
                            return super.resolveAlias(alias);
                        } catch (Exception e) {
                            return (Class<T>) Object.class;
                        }
                    }
                };
            }

        };

        System.out.println("开始检查MyBatis XML文件（处理动态表达式null值）：");
        System.out.println("目标目录：" + dir.getAbsolutePath());
        System.out.println("==========================================");

        // 遍历并解析所有XML文件
        traverseAndParseXmlFiles(dir, configuration);

        // 检查解析后的SQL语句
        checkMappedStatements(configuration, dirPath);

        System.out.println("==========================================");
        System.out.println("检查完成！");
    }
    /**
     * 从properties文件加载检查规则
     */
    private static boolean loadCheckRules(String configPath) {
        File configFile = new File(configPath);
        if (!configFile.exists() || !configFile.isFile()) {
            System.err.println("配置文件不存在：" + configPath);
            return false;
        }

        try (InputStreamReader isr = new InputStreamReader(
                new FileInputStream(configFile),
                StandardCharsets.UTF_8  // 明确指定UTF-8编码
        )) {

            Properties props = new Properties();

            props.load(isr);

            // 提取所有规则编号
            Set<Integer> ruleNumbers = new TreeSet<>();
            for (String key : props.stringPropertyNames()) {
                if (key.matches("rule\\.\\d+\\.regex")) {
                    int num = Integer.parseInt(key.replaceAll("rule\\.(\\d+)\\.regex", "$1"));
                    ruleNumbers.add(num);
                }
            }

            // 加载每条规则
            for (int num : ruleNumbers) {
                String regex = props.getProperty("rule." + num + ".regex");
                String msg = props.getProperty("rule." + num + ".msg");
                String suggestion = props.getProperty("rule." + num + ".suggestion");

                if (regex != null && msg != null && suggestion != null) {
                    CHECK_RULES.add(new CheckRule(regex, msg, suggestion));
                } else {
                    System.err.println("规则 " + num + " 配置不完整，已跳过");
                }
            }

            System.out.println("成功加载 " + CHECK_RULES.size() + " 条检查规则");
            return true;

        } catch (IOException e) {
            System.err.println("读取配置文件失败：" + e.getMessage());
            return false;
        }
    }

    private static void traverseAndParseXmlFiles(File dir, Configuration configuration) {
        // 用于记录已解析的文件路径，防止重复解析
        Set<String> parsedFiles = new HashSet<>();

        traverseFiles(dir, configuration, parsedFiles);
    }
    /**
     * 遍历目录并解析XML文件
     */
    private static void traverseFiles(File dir, Configuration configuration,Set<String> parsedFiles) {
        File[] files = dir.listFiles();
        if (files == null) return;

        for (File file : files) {
            if (file.isDirectory()) {
                traverseFiles(file, configuration,parsedFiles);
            } else if (file.getName().toLowerCase().endsWith(".xml")) {
                String filePath = file.getAbsolutePath().replace("\\","/");
                // 过滤编译目录（如target/、build/等）
                if (BUILD_DIR_PATTERN.matcher(filePath).find()) {
                    log.debug("跳过编译目录文件：" + filePath);
                    continue;
                }
                if (!isMyBatisMapperFile(file)) {
                    log.debug("跳过非MyBatis XML文件：" + file.getAbsolutePath());
                    continue;
                }
                // 检查是否已解析过该文件
                if (parsedFiles.contains(filePath)) {
                    log.debug("跳过重复解析的文件：" + filePath);
                    continue;
                }
                parsedFiles.add(filePath);
                try (InputStream is = new FileInputStream(file)) {
                    String resource = file.getAbsolutePath().replace("\\", "/");
                    XMLMapperBuilder mapperBuilder = new XMLMapperBuilder(
                            is, configuration, resource, configuration.getSqlFragments()
                    );
                    try {
                        mapperBuilder.parse();
                        log.debug("已解析文件：" + file.getAbsolutePath());
                    } catch (BuilderException e) {
                        if (e.getMessage().contains("ClassNotFoundException") ||
                                e.getMessage().contains("Could not resolve type alias")) {
                            log.debug("已跳过类验证解析文件：" + file.getAbsolutePath());
                        } else {
                            log.error("解析文件出错：" + file.getAbsolutePath() + "，错误：" + e.getMessage());
                        }
                    }
                } catch (Exception e) {
                    log.error("读取文件失败：" + file.getAbsolutePath() + "，错误：" + e.getMessage());
                }
            }
        }
    }
    /**
     * 判断是否为MyBatis映射文件（包含<mapper>标签）
     */
    private static boolean isMyBatisMapperFile(File xmlFile) {
        try {
            // 读取文件前1000字节判断（无需读取整个文件，提高效率）
            byte[] buffer = new byte[1000];
            try (InputStream is = new FileInputStream(xmlFile)) {
                int bytesRead = is.read(buffer);
                if (bytesRead > 0) {
                    String content = new String(buffer, 0, bytesRead, StandardCharsets.UTF_8);
                    return MYBATIS_MAPPER_PATTERN.matcher(content).find();
                }
            }
            return false;
        } catch (IOException e) {
            System.err.println("判断文件类型失败：" + xmlFile.getAbsolutePath() + "，错误：" + e.getMessage());
            return false;
        }
    }
    /**
     * 检查所有解析后的SQL语句（修正MappedStatement获取方式）
     */
    private static void checkMappedStatements(Configuration configuration, String baseDir) {
        // 修正：getMappedStatements()返回的是Collection而非Map
        List<MappedStatement> msCollection = configuration.getMappedStatements().stream().distinct().collect(Collectors.toList());

        for (int i = 0; i < msCollection.size(); i++) {
            if (msCollection.get(i) instanceof MappedStatement) {
                try {
                    MappedStatement ms = msCollection.get(i);
                    String msId = ms.getId(); // 获取SQL语句的唯一标识（namespace.id）

                    // 获取SQL内容（处理各种表达式错误）
                    String sql = getSqlWithFallback(ms, baseDir);
                    if (sql == null || sql.trim().isEmpty()) continue;

                    // 定位原始文件
                    String resource = ms.getResource();
                    File xmlFile = new File(resource);
                    if (!xmlFile.exists()) {
                        xmlFile = new File(baseDir, resource.replace("/", File.separator));
                    }

                    // 检查SQL中的Oracle语法
                    checkSqlContent(sql, msId, xmlFile);
                } catch (Exception e) {
                    System.err.println(e.getMessage());
                }
            }
        }
    }

    /**
     * 获取SQL内容，处理各种表达式错误（使用原始XML内容作为备选）
     */
    private static String getSqlWithFallback(MappedStatement ms, String baseDir) {
        try {
            // 尝试正常获取SQL，使用空参数映射
            BoundSql boundSql = ms.getBoundSql(new HashMap<>());
            return boundSql.getSql();
        } catch (Exception e) {
            // 处理所有表达式求值错误
            if (e.getMessage().contains("evaluated to a null value") ||
                    e.getMessage().contains("ew.") ||
                    e.getCause() instanceof OgnlException) {
                log.debug("检测到表达式求值错误（" + ms.getId() + "），使用原始XML内容进行检查");
                // 从原始XML文件中提取SQL片段作为备选
                return extractSqlFromXml(ms.getResource(), baseDir);
            } else {
                log.error("获取SQL失败（" + ms.getId() + "）：" + e.getMessage());
                return null;
            }
        }
    }

    /**
     * 从XML文件中直接提取SQL内容
     */
    private static String extractSqlFromXml(String resource, String baseDir) {
        try {
            File xmlFile = new File(resource);
            if (!xmlFile.exists()) {
                xmlFile = new File(baseDir, resource.replace("/", File.separator));
            }

            if (!xmlFile.exists()) {
                return null;
            }

            // 读取XML文件内容
            String xmlContent = new String(Files.readAllBytes(Paths.get(xmlFile.getPath())), StandardCharsets.UTF_8);

            // 1. 移除注释
            String noComments = xmlContent.replaceAll("<!--[\\s\\S]*?-->", "");

            // 2. 移除XML标签
            String noTags = XML_TAG_PATTERN.matcher(noComments).replaceAll(" ");

            // 3. 移除所有${}和#{}表达式
            String noDollarExp = DOLLAR_EXPRESSION_PATTERN.matcher(noTags).replaceAll("");
            String noHashExp = HASH_EXPRESSION_PATTERN.matcher(noDollarExp).replaceAll("");

            // 4. 清理多余空格
            return noHashExp.replaceAll("\\s+", " ").trim();
        } catch (Exception e) {
            log.error("提取XML原始SQL失败：" + e.getMessage(),e);
            return null;
        }
    }

    /**
     * 检查SQL内容中的Oracle语法
     */
    private static void checkSqlContent(String sql, String msId, File xmlFile) {
        boolean hasIssue = false;
        List<String> lines = null;
        String fileContent = null;

        try {
            if (xmlFile.exists()) {
                lines = Files.readAllLines(Paths.get(xmlFile.getPath()), StandardCharsets.UTF_8);
                fileContent = String.join("\n", lines);
            }
        } catch (IOException e) {
            // 忽略读取失败
        }

//        System.out.println("\n检查SQL：" + msId);
//        System.out.println("文件：" + (xmlFile.exists() ? xmlFile.getAbsolutePath() : "未知路径"));

        for (CheckRule rule : CHECK_RULES) {
            Matcher matcher = rule.pattern.matcher(sql);
            while (matcher.find()) {
                hasIssue = true;
                String matchedText = matcher.group();
                int lineNum = -1;

                if (fileContent != null) {
                    lineNum = getLineNumber(fileContent, matchedText);
                }
                System.out.println("\n检查SQL：" + msId);
                System.out.println("文件：" + (xmlFile.exists() ? xmlFile.getAbsolutePath() : "未知路径"));
                System.out.printf("  发现：%s%n", rule.errorMsg);
                System.out.printf("  匹配内容：%s%n", matchedText);
                System.out.printf("  行号（参考）：%d%n", lineNum);
                System.out.printf("  建议：%s%n%n", rule.suggestion);
            }
        }

        if (!hasIssue) {
//            System.out.println("  未发现Oracle特有语法");
        }
    }

    /**
     * 计算匹配内容在文件中的行号
     */
    private static int getLineNumber(String fileContent, String matchedText) {
        int index = fileContent.indexOf(matchedText);
        if (index == -1) {
            // 如果精确匹配失败，尝试模糊匹配
            String[] parts = matchedText.split("\\s+");
            for (String part : parts) {
                if (part.length() > 3) { // 只匹配有意义的片段
                    index = fileContent.indexOf(part);
                    if (index != -1) break;
                }
            }
        }
        if (index == -1) return -1;

        int lineNum = 1;
        for (int i = 0; i < index; i++) {
            if (fileContent.charAt(i) == '\n') {
                lineNum++;
            }
        }
        return lineNum;
    }

    /**
     * 检查规则实体类
     */
    static class CheckRule {
        Pattern pattern;
        String errorMsg;
        String suggestion;

        CheckRule(String regex, String errorMsg, String suggestion) {
            this.pattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE);
            this.errorMsg = errorMsg;
            this.suggestion = suggestion;
        }
    }
}

    